Leaflet Blog in Deno Fresh
1/** @jsxImportSource preact */ 2import { CSS, render } from "@deno/gfm"; 3import { Handlers, PageProps } from "$fresh/server.ts"; 4 5import { Layout } from "../../islands/layout.tsx"; 6import { PostInfo } from "../../components/post-info.tsx"; 7import { Title } from "../../components/typography.tsx"; 8import { getPost } from "../../lib/api.ts"; 9import { Head } from "$fresh/runtime.ts"; 10 11interface Post { 12 uri: string; 13 value: { 14 title?: string; 15 subtitle?: string; 16 content?: string; 17 createdAt?: string; 18 }; 19} 20 21// Only override backgrounds in dark mode to make them transparent 22const transparentDarkModeCSS = ` 23@media (prefers-color-scheme: dark) { 24 .markdown-body { 25 color: white; 26 background-color: transparent; 27 } 28 29 .markdown-body a { 30 color: #58a6ff; 31 } 32 33 .markdown-body blockquote { 34 border-left-color: #30363d; 35 background-color: transparent; 36 } 37 38 .markdown-body pre, 39 .markdown-body code { 40 background-color: transparent; 41 color: #c9d1d9; 42 } 43 44 .markdown-body table td, 45 .markdown-body table th { 46 border-color: #30363d; 47 background-color: transparent; 48 } 49} 50 51.font-sans { font-family: var(--font-sans); } 52.font-serif { font-family: var(--font-serif); } 53.font-mono { font-family: var(--font-mono); } 54 55.markdown-body h1 { 56 font-family: var(--font-serif); 57 text-transform: uppercase; 58 font-size: 2.25rem; 59} 60 61.markdown-body h2 { 62 font-family: var(--font-serif); 63 text-transform: uppercase; 64 font-size: 1.75rem; 65} 66 67.markdown-body h3 { 68 font-family: var(--font-serif); 69 text-transform: uppercase; 70 font-size: 1.5rem; 71} 72 73.markdown-body h4 { 74 font-family: var(--font-serif); 75 text-transform: uppercase; 76 font-size: 1.25rem; 77} 78 79.markdown-body h5 { 80 font-family: var(--font-serif); 81 text-transform: uppercase; 82 font-size: 1rem; 83} 84 85.markdown-body h6 { 86 font-family: var(--font-serif); 87 text-transform: uppercase; 88 font-size: 0.875rem; 89} 90`; 91 92export const handler: Handlers<Post> = { 93 async GET(_req, ctx) { 94 try { 95 const { slug } = ctx.params; 96 const post = await getPost(slug); 97 return ctx.render(post); 98 } catch (error) { 99 console.error("Error fetching post:", error); 100 return new Response("Post not found", { status: 404 }); 101 } 102 }, 103}; 104 105export default function BlogPage({ data: post }: PageProps<Post>) { 106 if (!post) { 107 return <div>Post not found</div>; 108 } 109 110 return ( 111 <> 112 <Head> 113 <title>{post.value.title} knotbin</title> 114 <meta 115 name="description" 116 content={post.value.subtitle || "by Roscoe Rubin-Rottenberg"} 117 /> 118 {/* Merge GFM's default styles with our dark-mode overrides */} 119 <style 120 dangerouslySetInnerHTML={{ __html: CSS + transparentDarkModeCSS }} 121 /> 122 </Head> 123 124 <Layout> 125 <div class="p-8 pb-20 gap-16 sm:p-20"> 126 <link rel="alternate" href={post.uri} /> 127 <div class="max-w-[600px] mx-auto"> 128 <article class="w-full space-y-8"> 129 <div class="space-y-4 w-full"> 130 <Title>{post.value.title || 'Untitled'}</Title> 131 {post.value.subtitle && ( 132 <p class="text-2xl md:text-3xl font-serif leading-relaxed max-w-prose"> 133 {post.value.subtitle} 134 </p> 135 )} 136 <PostInfo 137 content={post.value.content || ''} 138 createdAt={post.value.createdAt || new Date().toISOString()} 139 includeAuthor 140 className="text-sm" 141 /> 142 <div class="diagonal-pattern w-full h-3" /> 143 </div> 144 <div class="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0"> 145 <div 146 class="mt-8 markdown-body" 147 // replace old pds url with new one for blob urls 148 dangerouslySetInnerHTML={{ 149 __html: render(post.value.content || '').replace( 150 /puffball\.us-east\.host\.bsky\.network/g, 151 "knotbin.xyz", 152 ), 153 }} 154 /> 155 </div> 156 </article> 157 </div> 158 </div> 159 </Layout> 160 </> 161 ); 162}